

当动态值的类型为T时，每个VariantChoice基类负责处理初始化、赋值和销毁。本节通过填充VariantChoice类模板的详细信息，来实现这些操作。


\subsubsubsection{26.4.1\hspace{0.2cm}初始化}

从变量所存储的类型之一的值开始初始化变量，用双精度值初始化Variant<int, double, string>，可以通过VariantChoice的构造函数完成，它接受T类型的值:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantchoiceinit.hpp}
\begin{lstlisting}[style=styleCXX]
#include <utility> // for std::move()

template<typename T, typename... Types>
VariantChoice<T, Types...>::VariantChoice(T const& value) {
	// place value in buffer and set type discriminator:
	new(getDerived().getRawBuffer()) T(value);
	getDerived().setDiscriminator(Discriminator);
}

template<typename T, typename... Types>
VariantChoice<T, Types...>::VariantChoice(T&& value) {
	// place moved value in buffer and set type discriminator:
	new(getDerived().getRawBuffer()) T(std::move(value));
	getDerived().setDiscriminator(Discriminator);
}
\end{lstlisting}

构造函数都使用CRTP操作getDerived()访问共享缓冲区，然后执行new，用类型T的新值初始化存储。第一个构造函数复制构造传入值，而第二个构造函数移动构造传入值。

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}这里的构造使用，阻止了在Variant设计中使用引用类型。这个限制可以通过在类中包装引用来解决，比如std::reference\_wrapper。
\end{tcolorbox}

构造函数设置辨别值来指示变量存储的(动态)类型。

最终目标是能够用任何类型的值初始化变量，甚至是隐式转换。

\begin{lstlisting}[style=styleCXX]
Variant<int, double, string> v("hello"); // implicitly converted to string
\end{lstlisting}

为了实现这一点，通过引入using声明，将VariantChoice构造函数继承为Variant本身

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}在using声明(第4.4.5节)中介绍了包扩展的使用。C++17前，继承这些构造函数需要递归继承模式，类似于第25章中展示的Tuple公式。
\end{tcolorbox}

\begin{lstlisting}[style=styleCXX]
using VariantChoice<Types, Types...>::VariantChoice...;
\end{lstlisting}

这个using声明产生了Variant构造函数，从Types中的每个类型T复制或移动。对于Variant<int, double, string>，构造函数实际上是:

\begin{lstlisting}[style=styleCXX]
Variant(int const&);
Variant(int&&);
Variant(double const&);
Variant(double&&);
Variant(string const&);
Variant(string&&);
\end{lstlisting}

\subsubsubsection{26.4.2\hspace{0.2cm}销毁}

初始化Variant时，将在其缓冲区中构造一个值。destroy操作为该值的销毁过程:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantchoicedestroy.hpp}
\begin{lstlisting}[style=styleCXX]
template<typename T, typename... Types>
bool VariantChoice<T, Types...>::destroy() {
	if (getDerived().getDiscriminator() == Discriminator) {
		// if type matches, call placement delete:
		getDerived().template getBufferAs<T>()->~T();
		return true;
	}
	return false;
}
\end{lstlisting}

当辨别器匹配时，通过使用\texttt{->}~T()调用适当的析构函数，可以显式地销毁缓冲区中的内容。 
VariantChoice::destroy()操作只有在辨别器匹配时才有用。但我们通常希望销毁存储在Variant中的值，而不考虑当前的类型。因此，Variant::destroy()在基类中调用VariantChoice::destroy()的操作:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantdestroy.hpp}
\begin{lstlisting}[style=styleCXX]
template<typename... Types>
void Variant<Types...>::destroy() {
	// call destroy() on each VariantChoice base class; at most one will succeed:
	bool results[] = {
		VariantChoice<Types, Types...>::destroy()...
	};
	// indicate that the variant does not store a value
	this->setDiscriminator(0);
}
\end{lstlisting}

结果初始化式中的包展开确保对每个VariantChoice基类调用了destroy。这些调用中最多有一个会成功(匹配辨别器的那个)，而Variant为空。通过将辨别器的值设置为0来表示空状态。

数组结果本身只是提供了一个使用初始化列表的上下文;它的实际值会忽略。C++17中，可以使用折叠表达式(在第12.4.6节中讨论过)来消除这个变量:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantdestroy17.hpp}
\begin{lstlisting}[style=styleCXX]
template<typename... Types>
void Variant<Types...>::destroy()
{
	// call destroy() on each VariantChoice base class; at most one will succeed:
	(VariantChoice<Types, Types...>::destroy() , ...);
	
	// indicate that the variant does not store a value
	this->setDiscriminator(0);
}
\end{lstlisting}


\subsubsubsection{26.4.3\hspace{0.2cm}赋值}

赋值构建在初始化和销毁的基础上，如赋值操作符所示:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantchoiceassign.hpp}
\begin{lstlisting}[style=styleCXX]
template<typename T, typename... Types>
auto VariantChoice<T, Types...>::operator= (T const& value) -> Derived& {
	if (getDerived().getDiscriminator() == Discriminator) {
		// assign new value of same type:
		*getDerived().template getBufferAs<T>() = value;
	}
	else {
		// assign new value of different type:
		getDerived().destroy(); // try destroy() for all types
		new(getDerived().getRawBuffer()) T(value); // place new value
		getDerived().setDiscriminator(Discriminator);
	}
	return getDerived();
}

template<typename T, typename... Types>
auto VariantChoice<T, Types...>::operator= (T&& value) -> Derived& {
	if (getDerived().getDiscriminator() == Discriminator) {
		// assign new value of same type:
		*getDerived().template getBufferAs<T>() = std::move(value);
	}
	else {
		// assign new value of different type:
		getDerived().destroy(); // try destroy() for all types
		new(getDerived().getRawBuffer()) T(std::move(value)); // place new value
		getDerived().setDiscriminator(Discriminator);
	}
	return getDerived();
}
\end{lstlisting}

与从一种存储值类型进行初始化一样，每个VariantChoice提供一个赋值操作符，该操作符将其存储的值类型复制(或移动)到变量的存储中。这些赋值操作符由Variant通过以下using声明继承:

\begin{lstlisting}[style=styleCXX]
using VariantChoice<Types, Types...>::operator=...;
\end{lstlisting}

赋值操作符的实现有两条路径。若该变体已经存储了给定类型T的值(由辨别器匹配)，那么赋值操作符将根据需要，直接将类型T的值复制赋值或移动赋值到缓冲区中，辨别器不变。

若Variant没有存储T类型的值，赋值需要两个步骤:使用Variant:: Destroy()销毁当前值，然后使用new初始化T类型的新值，并设置辨别器。

使用new的两步赋值有三个常见问题，我们必须考虑:

\begin{itemize}
\item
自赋值

\item
异常

\item
std::launder()
\end{itemize}

\hspace*{\fill} \\ %插入空行
\noindent
\textbf{自赋值}

由于像下面这样的表达式，变量v可以自赋值:

\begin{lstlisting}[style=styleCXX]
v = v.get<T>()
\end{lstlisting}

上面实现的两步过程中，源值将在复制之前销毁，这可能会导致内存损坏。不过，自赋值意味着辨别器匹配，所以这样的代码将调用T的赋值操作符。

\hspace*{\fill} \\ %插入空行
\noindent
\textbf{异常}

若现有值的销毁完成，但new初始化引发异常，变量的状态是什么?我们的实现中，Variant::destroy()将标识符的值重置为0。非异常情况下，将在初始化完成后适当地设置辨别器。在初始化新值期间发生异常时，标识符保持0，表示该变量不存储值。我们的设计中，这是产生无值Variant的唯一方法。

下面的程序演示了如何通过复制构造函数，抛出的类型的值来生成无值Variant:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantexception.cpp}
\begin{lstlisting}[style=styleCXX]
include "variant.hpp"
#include <exception>
#include <iostream>
#include <string>

class CopiedNonCopyable : public std::exception
{
};

class NonCopyable
{
	public:
	NonCopyable() {
	}

	NonCopyable(NonCopyable const&) {
		throw CopiedNonCopyable();
	}
	NonCopyable(NonCopyable&&) = default;
	
	NonCopyable& operator= (NonCopyable const&) {
		throw CopiedNonCopyable();
	}

	NonCopyable& operator= (NonCopyable&&) = default;
	};

int main()
{
	Variant<int, NonCopyable> v(17);
	try {
		NonCopyable nc;
		v = nc;
	}
		catch (CopiedNonCopyable) {
		std::cout << "Copy assignment of NonCopyable failed." << ’\n’;
		if (!v.is<int>() && !v.is<NonCopyable>()) {
			std::cout << "Variant has no value." << ’\n’;
		}
	}
}
\end{lstlisting}

输出为:

\begin{tcblisting}{commandshell={}}
Copy assignment of NonCopyable failed.
Variant has no value.
\end{tcblisting}

对没有值的Variant访问，无论是通过get()还是通过下面一节描述的访问者机制，都会抛出EmptyVariant异常，并允许程序从这种异常情况中恢复。empty()成员函数检查变量可以检查，Variant实例是否处于空状态:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{variant/variantempty.hpp}
\begin{lstlisting}[style=styleCXX]
template<typename... Types>
bool Variant<Types...>::empty() const {
	return this->getDiscriminator() == 0;
}
\end{lstlisting}

两步赋值的第三个问题很微妙，C++标准化委员会直到C++17标准化结束时才意识到这个问题。下面先简要地解释一下。

\hspace*{\fill} \\ %插入空行
\noindent
\textbf{std::launder()}

C++编译器的目标通常是生成高性能的代码，而提高生成代码性能的主要机制可能是避免重复地将数据从内存复制到寄存器。为了做好这一点，编译器必须做出一些假设，其中一个假设是某些类型的数据在其生命周期内不可变。这包括const数据、引用(可以初始化，但之后不能修改)和存储在多态对象中的一些数据存储，这些对象用于分派虚函数、定位虚基类、处理typeid和static\_cast操作符。

上面两步赋值过程的问题是，偷偷地结束一个对象的生命周期，并以编译器可能无法识别的方式在相同的地方开始另一个对象的生命周期。编译器可能会假设，Variant对象的前一个状态获得的值仍然有效。而实际上，带有new初始化会使其失效。若不解决，将在获得良好性能而编译时，使用带有不可变数据成员的Variant类型可能偶尔会产生无效的结果。这样的错误通常很难查(一部分原因是它们很少发生，部另一分原因是它们在源代码中不可见)。

C++17后，这个问题的解决方案是通过std::laundry()访问新对象的地址，只返回实参，但这会导致编译器识别出结果地址指向的对象可能与编译器传递给std::laundry()的实参不同。但std::laundry()只修复它返回的地址，而不是传递给std::laundry()的参数，因为编译器用表达式来表示，而非实际地址(因为其在运行时才存在)。因此，在构造new的初始值之后，必须确保接下来的每个访问都进行数据“清洗”，这就是总是“清洗”指向Variant缓冲区指针的原因。有一些方法可以做得更好(添加一个指针成员，该成员引用缓冲区，并在每次赋值后通过new获得“清洗”地址)，但这会以难以维护的方式使代码复杂化。我们的方法只要通过getBufferAs()成员独占地访问缓冲区，简单又正确。

std::laundry()的并不完全令人满意:很微妙，很难察觉(直到本书快要出版之前才注意到)，而且很难缓解相关的问题(std::laundry()不太容易使用)。因此，委员会的几位成员需要在这方面做更多的工作，以找到一个更令人满意的解决办法。请参阅[JosuttisLaunder]以获得该问题的更详细描述。


